/*
 * Copyright 2011, Mysema Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.cloudlead.utils.persistence.codegen;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.nio.charset.Charset;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nullable;
import javax.validation.constraints.Size;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.cloudlead.common.worker.action.impl.AbstractCrudActionFactory;
import com.cloudlead.common.lang.BusinessModule;
import com.cloudlead.common.lang.annotation.Label;
import com.cloudlead.common.lang.service.CrudService;
import com.cloudlead.common.persistence.CrudRepository;
import com.cloudlead.common.persistence.impl.AbstractCrudRepository;
import com.cloudlead.common.persistence.impl.AbstractMultiTableCrudService;
import com.cloudlead.utils.persistence.codegen.serializer.SimpleActionFactorySerializer;
import com.cloudlead.utils.persistence.codegen.serializer.SimpleModulesSerializer;
import com.cloudlead.utils.persistence.codegen.serializer.SimpleVOSerializer;
import com.cloudlead.utils.persistence.codegen.type.BelongsToImpl;
import com.cloudlead.utils.persistence.codegen.type.BelongsTosImpl;
import com.cloudlead.utils.persistence.codegen.type.DomainImpl;
import com.cloudlead.utils.persistence.codegen.type.HasManyImpl;
import com.cloudlead.utils.persistence.codegen.type.HasManysImpl;
import com.cloudlead.utils.persistence.codegen.type.LabelImpl;
import com.cloudlead.utils.persistence.codegen.type.NotNullImpl;
import com.google.common.io.Files;
import com.mysema.codegen.CodeWriter;
import com.mysema.codegen.ScalaWriter;
import com.mysema.codegen.model.ClassType;
import com.mysema.codegen.model.SimpleType;
import com.mysema.codegen.model.Type;
import com.mysema.codegen.model.TypeCategory;
import com.mysema.query.codegen.CodegenModule;
import com.mysema.query.codegen.EntityType;
import com.mysema.query.codegen.Property;
import com.mysema.query.codegen.QueryTypeFactory;
import com.mysema.query.codegen.Serializer;
import com.mysema.query.codegen.SimpleSerializerConfig;
import com.mysema.query.codegen.Supertype;
import com.mysema.query.codegen.TypeMappings;
import com.mysema.query.sql.ColumnImpl;
import com.mysema.query.sql.ColumnMetadata;
import com.mysema.query.sql.Configuration;
import com.mysema.query.sql.SQLTemplates;
import com.mysema.query.sql.SQLTemplatesRegistry;
import com.mysema.query.sql.codegen.KeyDataFactory;
import com.mysema.query.sql.codegen.NamingStrategy;
import com.mysema.query.sql.codegen.SQLCodegenModule;
import com.mysema.query.sql.codegen.SpatialSupport;
import com.mysema.query.sql.support.ForeignKeyData;
import com.mysema.query.sql.support.InverseForeignKeyData;
import com.mysema.query.sql.support.PrimaryKeyData;
import com.mysema.query.sql.support.SizeImpl;

/**
 * SimpleMetaDataExporter exports JDBC metadata to Querydsl query types
 * <p>
 * <p>
 * Example
 * </p>
 * <p>
 * <pre>
 * SimpleMetaDataExporter exporter = new SimpleMetaDataExporter();
 * exporter.setPackageName(&quot;com.example.domain&quot;);
 * exporter.setTargetFolder(new File(&quot;target/generated-sources/java&quot;));
 * exporter.export(connection.getMetaData());
 * </pre>
 *
 * @author tiwe
 */
public class SimpleMetaDataExporter {

    private static final Logger logger = LoggerFactory
            .getLogger(SimpleMetaDataExporter.class);

    private final SQLTemplatesRegistry sqlTemplatesRegistry = new SQLTemplatesRegistry();

    private final SQLCodegenModule module = new SQLCodegenModule();

    private final Set<String> classes = new HashSet<String>();

    private File targetFolder;

    @Nullable
    private String basePackageName;

    @Nullable
    private String schemaPattern, tableNamePattern;

    @Nullable
    private Serializer beanSerializer;

    @Nullable
    private Serializer voSerializer;

    @Nullable
    private Serializer validatorGroupSerializer;

    @Nullable
    private Serializer daoIfcSerializer;

    @Nullable
    private Serializer daoImplSerializer;

    @Nullable
    private Serializer serviceIfcSerializer;

    @Nullable
    private Serializer serviceImplSerializer;

    @Nullable
    private Serializer actionFactorySerializer;

    @Nullable
    private Serializer modulesSerializer;

    private boolean createScalaSources = false;

    private final Map<EntityType, Type> entityToWrapped = new HashMap<EntityType, Type>();

    private Serializer metaDataSerializer;

    private TypeMappings typeMappings;

    private QueryTypeFactory queryTypeFactory;

    private NamingStrategy namingStrategy;

    private Configuration configuration;

    private KeyDataFactory keyDataFactory;

    private boolean columnAnnotations = false;

    private boolean validationAnnotations = false;

    private boolean schemaToPackage = false;

    private String sourceEncoding = "UTF-8";

    private boolean lowerCase = false;

    private boolean exportTables = true;

    private boolean exportViews = true;

    private boolean exportAll = false;

    private boolean exportPrimaryKeys = true;

    private boolean exportForeignKeys = true;

    private boolean exportBelongsTos = true;

    private boolean exportHasManys = false;

    private boolean spatial = false;

    private Map<String, Map<String, Class<?>>> registerDataTypes = new HashMap<String, Map<String, Class<?>>>();

    @Nullable
    private String tableTypesToExport;

    public SimpleMetaDataExporter() {
    }

    public void registerVoDataType(String voClassName, String property,
                                   Class<?> type) {
        Map<String, Class<?>> map = registerDataTypes.get(voClassName);
        if (null == map) {
            map = new HashMap<String, Class<?>>();
        }
        map.put(property, type);
        registerDataTypes.put(voClassName, map);
    }

    protected EntityType createEntityType(@Nullable String schemaName,
                                          String tableName, final String className) {
        EntityType classModel;

        if (beanSerializer == null) {
            String packageName = normalizePackage(module.getPackageName(),
                    schemaName);
            String simpleName = module.getPrefix() + className
                    + module.getSuffix();
            Type classTypeModel = new SimpleType(TypeCategory.ENTITY,
                    packageName + "." + simpleName, packageName, simpleName,
                    false, false);
            classModel = new EntityType(classTypeModel);
            typeMappings.register(classModel, classModel);

        } else {
            String beanPackageName = basePackageName + ".entity";
            String beanPackage = normalizePackage(beanPackageName, schemaName);
            String simpleName = module.getBeanPrefix() + className
                    + module.getBeanSuffix();
            Type classTypeModel = new SimpleType(TypeCategory.ENTITY,
                    beanPackage + "." + simpleName, beanPackage, simpleName,
                    false, false);
            classModel = new EntityType(classTypeModel);

            Type mappedType = queryTypeFactory.create(classModel);
            entityToWrapped.put(classModel, mappedType);
            typeMappings.register(classModel, mappedType);
        }

        classModel.getData().put("schema", schemaName);
        classModel.getData().put("table", tableName);
        return classModel;
    }

    protected EntityType createVOEntityType(@Nullable String schemaName,
                                            String tableName, final String className) {
        EntityType classModel;

        if (beanSerializer == null) {
            String packageName = normalizePackage(module.getPackageName(),
                    schemaName);
            String simpleName = className;
            Type classTypeModel = new SimpleType(TypeCategory.ENTITY,
                    packageName + "." + simpleName, packageName, simpleName,
                    false, false);
            classModel = new EntityType(classTypeModel);
            typeMappings.register(classModel, classModel);

        } else {
            String voPackageName = basePackageName + ".vo";
            String beanPackage = normalizePackage(voPackageName, schemaName);
            String simpleName = className;
            Type classTypeModel = new SimpleType(TypeCategory.ENTITY,
                    beanPackage + "." + simpleName, beanPackage, simpleName,
                    false, false);
            classModel = new EntityType(classTypeModel);

        }

        classModel.getData().put("schema", schemaName);
        classModel.getData().put("table", tableName);
        return classModel;
    }

    private String normalizePackage(String packageName,
                                    @Nullable String schemaName) {
        if (schemaToPackage && schemaName != null) {
            return namingStrategy.appendSchema(packageName, schemaName);
        } else {
            return packageName;
        }
    }

    protected Property createProperty(EntityType classModel,
                                      String normalizedColumnName, String propertyName,
                                      String escapedName, Type typeModel) {
        return new Property(classModel, propertyName, escapedName, typeModel,
                Collections.<String>emptyList(), false);
    }

    /**
     * Export the tables based on the given database metadata
     *
     * @param md
     * @param stmt
     * @throws SQLException
     */
    public void export(DatabaseMetaData md, Statement stmt) throws SQLException {
        if (basePackageName == null) {
            basePackageName = module.getPackageName();
        }
        module.bind(SQLCodegenModule.PACKAGE_NAME, basePackageName + ".query");

        String beanPackageName = basePackageName + ".entity";
        module.bind(SQLCodegenModule.BEAN_PACKAGE_NAME, beanPackageName);

        if (spatial) {
            SpatialSupport.addSupport(module);
        }

        typeMappings = module.get(TypeMappings.class);
        queryTypeFactory = module.get(QueryTypeFactory.class);
        metaDataSerializer = module.get(Serializer.class);
        beanSerializer = module.get(Serializer.class,
                SQLCodegenModule.BEAN_SERIALIZER);
        namingStrategy = module.get(NamingStrategy.class);
        configuration = module.get(Configuration.class);

        SQLTemplates templates = sqlTemplatesRegistry.getTemplates(md);
        if (templates != null) {
            configuration.setTemplates(templates);
        } else {
            logger.info("Found no specific dialect for "
                    + md.getDatabaseProductName());
        }

        if (beanSerializer == null) {
            keyDataFactory = new KeyDataFactory(namingStrategy,
                    module.getPackageName(), module.getPrefix(),
                    module.getSuffix(), schemaToPackage);
        } else {
            keyDataFactory = new KeyDataFactory(namingStrategy,
                    beanPackageName, module.getBeanPrefix(),
                    module.getBeanSuffix(), schemaToPackage);
        }

        String[] typesArray = null;

        if (tableTypesToExport != null && !tableTypesToExport.isEmpty()) {
            List<String> types = new ArrayList<String>();
            for (String tableType : tableTypesToExport.split(",")) {
                types.add(tableType.trim());
            }
            typesArray = types.toArray(new String[types.size()]);
        } else if (!exportAll) {
            List<String> types = new ArrayList<String>(2);
            if (exportTables) {
                types.add("TABLE");
            }
            if (exportViews) {
                types.add("VIEW");
            }
            typesArray = types.toArray(new String[types.size()]);
        }

        Map<String, String> modules = new HashMap<String, String>();

        if (tableNamePattern != null && tableNamePattern.contains(",")) {
            for (String table : tableNamePattern.split(",")) {
                ResultSet tables = md.getTables(null, schemaPattern,
                        null, typesArray);
                try {
                    while (tables.next()) {
                        String tableName = normalize(tables.getString("TABLE_NAME"));
                        if (tableName.matches(table)) {
                            addModule(stmt, modules, tableName);
                            handleTable(md, tables, stmt);
                        }
                    }
                } finally {
                    tables.close();
                }
            }
        } else {
            ResultSet tables = md.getTables(null, schemaPattern,
                    null, typesArray);
            try {
                while (tables.next()) {
                    String tableName = normalize(tables.getString("TABLE_NAME"));
                    if (null == tableNamePattern || tableName.matches(tableNamePattern)) {
                        addModule(stmt, modules, tableName);
                        handleTable(md, tables, stmt);
                    }
                }
            } finally {
                tables.close();
            }
        }

        serializeModules(modules);
        stmt.close();
    }

    private void serializeModules(Map<String, String> modules) {
        try {
            String simpleName = "BusinessModules";
            Type classTypeModel = new SimpleType(TypeCategory.ENUM,
                    basePackageName + "." + simpleName, basePackageName,
                    simpleName, false, false);
            EntityType classModel = new EntityType(classTypeModel);
            classModel.addSupertype(new Supertype(new ClassType(
                    BusinessModule.class)));
            classModel.addProperty(new Property(classModel, "label",
                    new ClassType(String.class)));
            classModel.addProperty(new Property(classModel, "parent",
                    new ClassType(BusinessModule.class)));

            String fileSuffix = createScalaSources ? ".scala" : ".java";
            String modulesPath = basePackageName.replace('.', '/') + "/"
                    + simpleName + fileSuffix;
            if (null != modulesSerializer) {
                SimpleModulesSerializer simpleModulesSerializer = (SimpleModulesSerializer) modulesSerializer;
                simpleModulesSerializer.setModules(modules);
                write(modulesSerializer, modulesPath, classModel);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void addModule(Statement stmt, Map<String, String> modules,
                           String tableName) throws SQLException {
        String normalizedTableName = namingStrategy
                .normalizeTableName(tableName);
        String className = namingStrategy.getClassName(normalizedTableName);
        String tableComment = getTableComment(stmt, tableName);
        modules.put(className, null == tableComment ? className : tableComment);
    }

    private String getTableComment(Statement stmt, String table)
            throws SQLException {
        ResultSet rs = stmt.executeQuery("SHOW CREATE TABLE " + table);
        String comment = null;
        if (rs != null && rs.next()) {
            String create = rs.getString(2);
            comment = parse(create);
        }
        rs.close();
        return comment;
    }

    String parse(String all) {
        String comment = null;
        int index = all.indexOf("COMMENT='");
        if (index < 0) {
            return "";
        }
        comment = all.substring(index + 9);
        comment = comment.substring(0, comment.length() - 1);
        return comment;
    }

    Set<String> getClasses() {
        return classes;
    }

    private void handleColumn(EntityType classModel, String tableName,
                              ResultSet columns) throws SQLException {
        String columnName = normalize(columns.getString("COLUMN_NAME"));
        String normalizedColumnName = namingStrategy
                .normalizeColumnName(columnName);
        int columnType = columns.getInt("DATA_TYPE");
        String typeName = columns.getString("TYPE_NAME");
        Number columnSize = (Number) columns.getObject("COLUMN_SIZE");
        Number columnDigits = (Number) columns.getObject("DECIMAL_DIGITS");
        int columnIndex = columns.getInt("ORDINAL_POSITION");
        int nullable = columns.getInt("NULLABLE");
        String remarks = columns.getString("REMARKS");

        String propertyName = namingStrategy.getPropertyName(
                normalizedColumnName, classModel);
        Class<?> clazz = configuration.getJavaType(columnType, typeName,
                columnSize != null ? columnSize.intValue() : 0,
                columnDigits != null ? columnDigits.intValue() : 0, tableName,
                columnName);
        if (clazz == null) {
            throw new IllegalStateException("Found no mapping for "
                    + columnType + " (" + tableName + "." + columnName + " "
                    + typeName + ")");
        }
        TypeCategory fieldType = TypeCategory.get(clazz.getName());
        if (Number.class.isAssignableFrom(clazz)) {
            fieldType = TypeCategory.NUMERIC;
        } else if (Enum.class.isAssignableFrom(clazz)) {
            fieldType = TypeCategory.ENUM;
        }
        Type typeModel = new ClassType(fieldType, clazz);
        Property property = createProperty(classModel, normalizedColumnName,
                propertyName, propertyName, typeModel);
        ColumnMetadata column = ColumnMetadata.named(normalizedColumnName)
                .ofType(columnType).withIndex(columnIndex);
        if (nullable == DatabaseMetaData.columnNoNulls) {
            column = column.notNull();
        }
        if (columnSize != null) {
            column = column.withSize(columnSize.intValue());
        }
        if (columnDigits != null) {
            column = column.withDigits(columnDigits.intValue());
        }
        property.getData().put("COLUMN", column);

        if (columnAnnotations) {
            property.addAnnotation(new ColumnImpl(normalizedColumnName));
        }
        if (validationAnnotations) {
            if (nullable == DatabaseMetaData.columnNoNulls) {
                NotNullImpl annotation = new NotNullImpl();
                property.addAnnotation(annotation);
            }
            int size = columns.getInt("COLUMN_SIZE");
            if (size > 0 && clazz.equals(String.class)) {
                property.addAnnotation(new SizeImpl(0, size));
            }
        }

        LabelImpl label = new LabelImpl(
                StringUtils.isBlank(remarks) ? propertyName : remarks);
        property.addAnnotation(label);

        classModel.addProperty(property);
    }

    @SuppressWarnings("unchecked")
    private void handleTable(DatabaseMetaData md, ResultSet tables,
                             Statement stmt) throws SQLException {
        String catalog = tables.getString("TABLE_CAT");
        String schema = tables.getString("TABLE_SCHEM");
        String schemaName = normalize(tables.getString("TABLE_SCHEM"));
        String tableName = normalize(tables.getString("TABLE_NAME"));
        String comment = getTableComment(stmt, tableName);
        String normalizedTableName = namingStrategy
                .normalizeTableName(tableName);
        String className = namingStrategy.getClassName(normalizedTableName);
        EntityType classModel = createEntityType(schemaName,
                normalizedTableName, className);
        EntityType voClassModel = createVOEntityType(schemaName,
                normalizedTableName, className);

        if (exportPrimaryKeys) {
            // collect primary keys
            Map<String, PrimaryKeyData> primaryKeyData = keyDataFactory
                    .getPrimaryKeys(md, catalog, schema, tableName);
            if (!primaryKeyData.isEmpty()) {
                classModel.getData().put(PrimaryKeyData.class,
                        primaryKeyData.values());
            }
        }

        if (exportForeignKeys) {
            // collect foreign keys
            Map<String, ForeignKeyData> foreignKeyData = keyDataFactory
                    .getImportedKeys(md, catalog, schema, tableName);
            if (!foreignKeyData.isEmpty()) {
                classModel.getData().put(ForeignKeyData.class,
                        foreignKeyData.values());
            }

            // collect inverse foreign keys
            Map<String, InverseForeignKeyData> inverseForeignKeyData = keyDataFactory
                    .getExportedKeys(md, catalog, schema, tableName);
            if (!inverseForeignKeyData.isEmpty()) {
                classModel.getData().put(InverseForeignKeyData.class,
                        inverseForeignKeyData.values());
            }
        }

        // collect columns
        ResultSet columns = md.getColumns(catalog, schema,
                tableName.replace("/", "//"), null);
        try {
            while (columns.next()) {
                handleColumn(classModel, tableName, columns);
            }
        } finally {
            columns.close();
        }

        // serialize model
        LabelImpl label = new LabelImpl(
                StringUtils.isBlank(comment) ? className : comment);
        classModel.addAnnotation(label);
        serialize(classModel);

        Set<Property> properties = classModel.getProperties();
        Map<String, Class<?>> dataTypeMap = registerDataTypes.get(voClassModel
                .getSimpleName());

        String voPackageName = basePackageName + ".vo";
        List<BelongsToImpl> belongsTos = new ArrayList<BelongsToImpl>();
        for (Property property : properties) {
            Type propertyType = property.getType();
            boolean customType = false;
            String propertyName = property.getName();
            if (null != dataTypeMap) {
                Class<?> propertyCls = dataTypeMap.get(propertyName);
                if (null != propertyCls) {
                    Type classType = new ClassType(propertyCls);
                    propertyType = classType;
                    customType = true;
                }
            }

            Collection<ForeignKeyData> foreignKeyDatas = (Collection<ForeignKeyData>) classModel
                    .getData().get(ForeignKeyData.class);
            boolean isForeignKey = false;

            if (exportBelongsTos && null != foreignKeyDatas
                    && !foreignKeyDatas.isEmpty()) {
                for (ForeignKeyData foreignKey : foreignKeyDatas) {
                    String propertyName2 = namingStrategy.getPropertyName(
                            foreignKey.getForeignColumns().get(0), classModel);
                    if (propertyName.equals(propertyName2)) {
                        String simpleName = foreignKey.getType()
                                .getSimpleName();
                        String substring = simpleName.substring(module
                                .getPrefix().length(), simpleName.length());
                        propertyType = new SimpleType(voPackageName + "."
                                + substring, voPackageName, substring);
                        propertyName = propertyName2.substring(0,
                                propertyName2.length() - 2);
                        belongsTos.add(new BelongsToImpl(propertyName,
                                propertyName2));
                        isForeignKey = true;
                        break;
                    }
                }
            }

            Property property2 = createProperty(voClassModel, propertyName,
                    propertyName, propertyName, propertyType);
            Collection<Annotation> annotations = property.getAnnotations();
            for (Annotation annotation : annotations) {
                if ((isForeignKey || customType)
                        && Size.class.equals(annotation.annotationType())) {
                    continue;
                }
                property2.addAnnotation(annotation);
            }
            voClassModel.addProperty(property2);
        }

        Collection<InverseForeignKeyData> inverseForeignKeyDatas = (Collection<InverseForeignKeyData>) classModel
                .getData().get(InverseForeignKeyData.class);
        List<HasManyImpl> hasManys = new ArrayList<HasManyImpl>();
        if (exportHasManys && null != inverseForeignKeyDatas
                && !inverseForeignKeyDatas.isEmpty()) {
            for (InverseForeignKeyData inverseForeignKeyData : inverseForeignKeyDatas) {
                String simpleName = inverseForeignKeyData.getType()
                        .getSimpleName();
                String substring = simpleName.substring(module.getPrefix()
                        .length(), simpleName.length());
                Type propertyType = new SimpleType(voPackageName + "."
                        + substring, voPackageName, substring);
                Type propertyListType = new ClassType(TypeCategory.LIST,
                        List.class, propertyType);
                String propertyName2 = namingStrategy.getPropertyName(
                        inverseForeignKeyData.getTable(), classModel);
                String propertyName = propertyName2 + "s";
                Set<String> propertyNames = voClassModel.getPropertyNames();
                int index = 0;
                while (propertyNames.contains(propertyName)) {
                    index++;
                    propertyName = propertyName2 + index + "s";
                }
                Property property2 = createProperty(voClassModel, propertyName,
                        propertyName, propertyName, propertyListType);

                String tableComment = getTableComment(stmt,
                        inverseForeignKeyData.getTable());
                LabelImpl tableLabel = new LabelImpl(
                        StringUtils.isBlank(tableComment) ? propertyName
                                : tableComment + "列表"
                                + (0 < index ? index : ""));
                property2.addAnnotation(tableLabel);

                voClassModel.addProperty(property2);
                hasManys.add(new HasManyImpl(propertyName, namingStrategy
                        .getPropertyName(inverseForeignKeyData
                                .getParentColumns().get(0), classModel)));
            }
        }

        if (!belongsTos.isEmpty()) {
            voClassModel.addAnnotation(new BelongsTosImpl(belongsTos
                    .toArray(new BelongsToImpl[belongsTos.size()])));
        }
        if (!hasManys.isEmpty()) {
            voClassModel.addAnnotation(new HasManysImpl(hasManys
                    .toArray(new HasManyImpl[hasManys.size()])));
        }
        voClassModel.addAnnotation(label);
        serializeVO(voClassModel, classModel);

        logger.info("Exported " + tableName + " successfully");
    }

    private String normalize(String str) {
        if (lowerCase && str != null) {
            return str.toLowerCase();
        } else {
            return str;
        }
    }

    private void serialize(EntityType type) {
        try {
            Label entityLabelAnnotation = type.getAnnotation(Label.class);
            String entityLabel = entityLabelAnnotation.value();

            String fileSuffix = createScalaSources ? ".scala" : ".java";

            String beanPackageName = basePackageName + ".entity";
            String daoPackageName = basePackageName + ".repository";

            if (beanSerializer != null) {
                String packageName = normalizePackage(beanPackageName,
                        (String) type.getData().get("schema"));
                String path = packageName.replace('.', '/') + "/"
                        + type.getSimpleName() + fileSuffix;
                write(beanSerializer, path, type);

                String otherPath = entityToWrapped.get(type).getFullName()
                        .replace('.', '/')
                        + fileSuffix;
                write(metaDataSerializer, otherPath, type);

                // 写dao接口
                if (daoIfcSerializer != null) {
                    String daoIfcPkgName = normalizePackage(daoPackageName,
                            (String) type.getData().get("schema"));
                    String simpleName = type.getSimpleName() + "Repository";
                    String daoIfcPath = daoIfcPkgName.replace('.', '/') + "/"
                            + simpleName + fileSuffix;

                    Type argtype = new ClassType(String.class);
                    Type superType = new ClassType(CrudRepository.class, type,
                            argtype);
                    Supertype entityType = new Supertype(superType);

                    Type classTypeModel = new SimpleType(TypeCategory.ENTITY,
                            daoIfcPkgName + "." + simpleName, daoIfcPkgName,
                            simpleName, false, false);
                    PEntityType classModel = new PEntityType(classTypeModel);
                    classModel.addSupertype(entityType);

                    classModel.addAnnotation(new LabelImpl(entityLabel + "存储"));
                    write(daoIfcSerializer, daoIfcPath, classModel);

                    // 写dao实现类
                    if (daoImplSerializer != null) {
                        String daoImplPkgName = daoIfcPkgName + ".impl";
                        String daoImplSimpleName = simpleName + "Impl";
                        String daoImplPath = daoImplPkgName.replace('.', '/')
                                + "/" + daoImplSimpleName + fileSuffix;

                        Type daoImplArgtype = new ClassType(String.class);
                        Type daoImplSuperType = new ClassType(
                                AbstractCrudRepository.class, type,
                                daoImplArgtype);
                        Supertype daoImplEntityType = new Supertype(
                                daoImplSuperType);

                        Type daoImplClassTypeModel = new SimpleType(
                                TypeCategory.ENTITY, daoImplPkgName + "."
                                + daoImplSimpleName, daoImplPkgName,
                                daoImplSimpleName, false, false);
                        PEntityType daoImplClassModel = new PEntityType(
                                daoImplClassTypeModel, daoImplEntityType);
                        daoImplClassModel
                                .addSupertype(new Supertype(classModel));
                        daoImplClassModel.addAnnotation(new LabelImpl(
                                entityLabel + "存储实现"));
                        write(daoImplSerializer, daoImplPath, daoImplClassModel);

                    }
                }
            } else {
                String packageName = normalizePackage(module.getPackageName(),
                        (String) type.getData().get("schema"));
                String path = packageName.replace('.', '/') + "/"
                        + type.getSimpleName() + fileSuffix;
                write(metaDataSerializer, path, type);
            }

        } catch (IOException e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    private void serializeVO(EntityType type, EntityType beanType) {
        try {

            Label entityLabelAnnotation = type.getAnnotation(Label.class);
            String entityLabel = entityLabelAnnotation.value();
            String fileSuffix = createScalaSources ? ".scala" : ".java";

            String voPackageName = basePackageName + ".vo";
            String daoPackageName = basePackageName + ".repository";
            String servicePackageName = basePackageName + ".service";
            String actionPackageName = basePackageName + ".action";
            String validatorGroupPackageName = actionPackageName + ".validator";
            Type validatorGroupClass = null;
            // 写validatorGroup接口
            if (validatorGroupSerializer != null) {

                String validatorGroupPkgName = normalizePackage(
                        validatorGroupPackageName,
                        (String) type.getData().get("schema"));
                String simpleName = type.getSimpleName() + "ValidatorGroup";
                String validatorGroupPath = validatorGroupPkgName.replace('.',
                        '/') + "/" + simpleName + fileSuffix;

                validatorGroupClass = new SimpleType(TypeCategory.ENTITY,
                        validatorGroupPkgName + "." + simpleName,
                        validatorGroupPkgName, simpleName, false, false);
                PEntityType classModel = new PEntityType(validatorGroupClass);

                classModel.addAnnotation(new LabelImpl(entityLabel + "验证组"));
                write(validatorGroupSerializer, validatorGroupPath, classModel);
            }

            if (voSerializer != null) {
                String packageName = normalizePackage(voPackageName,
                        (String) type.getData().get("schema"));
                String path = packageName.replace('.', '/') + "/"
                        + type.getSimpleName() + fileSuffix;
                DomainImpl domainImpl = new DomainImpl(beanType.getFullName());
                type.addAnnotation(domainImpl);
                SimpleVOSerializer simpleVOSerializer = (SimpleVOSerializer) voSerializer;
                simpleVOSerializer
                        .setValidatorGroupClassName(validatorGroupClass
                                .getFullName());

                write(voSerializer, path, type);

                // 写servie接口
                if (serviceIfcSerializer != null) {

                    String serviceIfcPkgName = normalizePackage(
                            servicePackageName,
                            (String) type.getData().get("schema"));
                    String simpleName = type.getSimpleName() + "Service";
                    String serviceIfcPath = serviceIfcPkgName.replace('.', '/')
                            + "/" + simpleName + fileSuffix;

                    Type superType = new ClassType(CrudService.class, type);
                    Supertype entityType = new Supertype(superType);

                    Type classTypeModel = new SimpleType(TypeCategory.ENTITY,
                            serviceIfcPkgName + "." + simpleName,
                            serviceIfcPkgName, simpleName, false, false);
                    PEntityType classModel = new PEntityType(classTypeModel);
                    classModel.addSupertype(entityType);

                    classModel.addAnnotation(new LabelImpl(entityLabel + "服务"));
                    write(serviceIfcSerializer, serviceIfcPath, classModel);

                    // 写service实现类
                    if (serviceImplSerializer != null) {
                        String serviceImplPkgName = serviceIfcPkgName + ".impl";
                        String serviceImplSimpleName = simpleName + "Impl";
                        String serviceImplPath = serviceImplPkgName.replace(
                                '.', '/')
                                + "/"
                                + serviceImplSimpleName
                                + fileSuffix;

                        String daoTypeName = beanType.getSimpleName()
                                + "Repository";
                        Type daoclassTypeModel = new SimpleType(
                                TypeCategory.ENTITY, daoPackageName + "."
                                + daoTypeName, daoPackageName,
                                daoTypeName, false, false);

                        Type serviceImplSuperType = new ClassType(
                                AbstractMultiTableCrudService.class,
                                daoclassTypeModel, type, beanType);
                        Supertype serviceImplEntityType = new Supertype(
                                serviceImplSuperType);

                        Type serviceImplClassTypeModel = new SimpleType(
                                TypeCategory.ENTITY, serviceImplPkgName + "."
                                + serviceImplSimpleName,
                                serviceImplPkgName, serviceImplSimpleName,
                                false, false);
                        PEntityType serviceImplClassModel = new PEntityType(
                                serviceImplClassTypeModel,
                                serviceImplEntityType);
                        serviceImplClassModel.addSupertype(new Supertype(
                                classModel));
                        serviceImplClassModel.addAnnotation(new LabelImpl(
                                entityLabel + "服务实现"));
                        write(serviceImplSerializer, serviceImplPath,
                                serviceImplClassModel);

                    }

                    // 写actionFactory实现类
                    if (actionFactorySerializer != null) {
                        String actionFactoryPkgName = actionPackageName;
                        String actionFactorySimpleName = type.getSimpleName()
                                + "ActionFactory";
                        String actionFactoryPath = actionFactoryPkgName.replace(
                                '.', '/')
                                + "/"
                                + actionFactorySimpleName
                                + fileSuffix;

                        Type actionFactorySuperType = new ClassType(
                                AbstractCrudActionFactory.class, classTypeModel, type);
                        Supertype actionFactoryEntityType = new Supertype(
                                actionFactorySuperType);

                        Type actionFactoryClassTypeModel = new SimpleType(
                                TypeCategory.ENTITY, actionFactoryPkgName + "."
                                + actionFactorySimpleName,
                                actionFactoryPkgName, actionFactorySimpleName,
                                false, false);
                        PEntityType actionFactoryClassModel = new PEntityType(
                                actionFactoryClassTypeModel,
                                actionFactoryEntityType);
                        SimpleActionFactorySerializer simpleActionFactorySerializer = (SimpleActionFactorySerializer) actionFactorySerializer;
                        simpleActionFactorySerializer
                                .setValidatorGroupClassName(validatorGroupClass
                                        .getFullName());

                        actionFactoryClassModel.addAnnotation(new LabelImpl(
                                entityLabel + "管理"));
                        write(actionFactorySerializer, actionFactoryPath,
                                actionFactoryClassModel);

                    }
                }

            } else {
                String packageName = normalizePackage(module.getPackageName(),
                        (String) type.getData().get("schema"));
                String path = packageName.replace('.', '/') + "/"
                        + type.getSimpleName() + fileSuffix;
                write(metaDataSerializer, path, type);
            }

        } catch (IOException e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    private void write(Serializer serializer, String path, EntityType type)
            throws IOException {
        File targetFile = new File(targetFolder, path);
        classes.add(targetFile.getPath());
        StringWriter w = new StringWriter();
        CodeWriter writer = createScalaSources ? new ScalaWriter(w)
                : new MyJavaWriter(w);
        serializer.serialize(type, SimpleSerializerConfig.DEFAULT, writer);

        // conditional creation
        boolean generate = true;
        byte[] bytes = w.toString().getBytes(sourceEncoding);
        if (targetFile.exists() && targetFile.length() == bytes.length) {
            String str = Files.toString(targetFile,
                    Charset.forName(sourceEncoding));
            if (str.equals(w.toString())) {
                generate = false;
            }
        } else {
            targetFile.getParentFile().mkdirs();
        }

        if (generate) {
            Files.write(bytes, targetFile);
        }
    }

    /**
     * Set the schema pattern filter to be used
     *
     * @param schemaPattern a schema name pattern; must match the schema name as it is
     *                      stored in the database; "" retrieves those without a schema;
     *                      {@code null} means that the schema name should not be used to
     *                      narrow the search (default: null)
     */
    public void setSchemaPattern(@Nullable String schemaPattern) {
        this.schemaPattern = schemaPattern;
    }

    /**
     * Set the table name pattern filter to be used
     *
     * @param tableNamePattern a table name pattern; must match the table name as it is
     *                         stored in the database (default: null)
     */
    public void setTableNamePattern(@Nullable String tableNamePattern) {
        this.tableNamePattern = tableNamePattern;
    }

    /**
     * Override the configuration
     *
     * @param configuration override configuration for custom type mappings etc
     */
    public void setConfiguration(Configuration configuration) {
        module.bind(Configuration.class, configuration);
    }

    /**
     * Set true to create Scala sources instead of Java sources
     *
     * @param createScalaSources whether to create Scala sources (default: false)
     */
    public void setCreateScalaSources(boolean createScalaSources) {
        this.createScalaSources = createScalaSources;
    }

    /**
     * Set the target folder
     *
     * @param targetFolder target source folder to create the sources into (e.g.
     *                     target/generated-sources/java)
     */
    public void setTargetFolder(File targetFolder) {
        this.targetFolder = targetFolder;
    }

    /**
     * Override the bean package name (default: packageName)
     *
     * @param packageName
     */
    public void setPackageName(@Nullable String packageName) {
        this.basePackageName = packageName;
    }

    /**
     * Override the name prefix for the classes (default: Q)
     *
     * @param namePrefix name prefix for query-types (default: Q)
     */
    public void setNamePrefix(String namePrefix) {
        module.bind(CodegenModule.PREFIX, namePrefix);
    }

    /**
     * Override the name suffix for the classes (default: "")
     *
     * @param nameSuffix name suffix for query-types (default: "")
     */
    public void setNameSuffix(String nameSuffix) {
        module.bind(CodegenModule.SUFFIX, nameSuffix);
    }

    /**
     * Override the bean prefix for the classes (default: "")
     *
     * @param beanPrefix bean prefix for bean-types (default: "")
     */
    public void setBeanPrefix(String beanPrefix) {
        module.bind(SQLCodegenModule.BEAN_PREFIX, beanPrefix);
    }

    /**
     * Override the bean suffix for the classes (default: "")
     *
     * @param beanSuffix bean suffix for bean-types (default: "")
     */
    public void setBeanSuffix(String beanSuffix) {
        module.bind(SQLCodegenModule.BEAN_SUFFIX, beanSuffix);
    }

    public void setVoSerializer(Serializer voSerializer) {
        this.voSerializer = voSerializer;
    }

    /**
     * Override the NamingStrategy (default: new DefaultNamingStrategy())
     *
     * @param namingStrategy namingstrategy to override (default: new
     *                       DefaultNamingStrategy())
     */
    public void setNamingStrategy(NamingStrategy namingStrategy) {
        module.bind(NamingStrategy.class, namingStrategy);
    }

    /**
     * Set the Bean serializer to create bean types as well
     *
     * @param beanSerializer serializer for JavaBeans (default: null)
     */
    public void setBeanSerializer(@Nullable Serializer beanSerializer) {
        module.bind(SQLCodegenModule.BEAN_SERIALIZER, beanSerializer);
    }

    /**
     * Set the Bean serializer class to create bean types as well
     *
     * @param beanSerializerClass serializer for JavaBeans (default: null)
     */
    public void setBeanSerializerClass(
            Class<? extends Serializer> beanSerializerClass) {
        module.bind(SQLCodegenModule.BEAN_SERIALIZER, beanSerializerClass);
    }

    /**
     * @param innerClassesForKeys
     */
    public void setInnerClassesForKeys(boolean innerClassesForKeys) {
        module.bind(SQLCodegenModule.INNER_CLASSES_FOR_KEYS,
                innerClassesForKeys);
    }

    /**
     * @param columnComparatorClass
     */
    public void setColumnComparatorClass(
            Class<? extends Comparator<Property>> columnComparatorClass) {
        module.bind(SQLCodegenModule.COLUMN_COMPARATOR, columnComparatorClass);
    }

    /**
     * @param serializerClass
     */
    public void setMetadataSerializerClass(
            Class<? extends Serializer> serializerClass) {
        module.bind(Serializer.class, serializerClass);
    }

    public void setDaoIfcSerializer(Serializer daoIfcSerializer) {
        this.daoIfcSerializer = daoIfcSerializer;
    }

    public void setDaoImplSerializer(Serializer daoImplSerializer) {
        this.daoImplSerializer = daoImplSerializer;
    }

    public void setValidatorGroupSerializer(Serializer validatorGroupSerializer) {
        this.validatorGroupSerializer = validatorGroupSerializer;
    }

    public void setServiceIfcSerializer(Serializer serviceIfcSerializer) {
        this.serviceIfcSerializer = serviceIfcSerializer;
    }

    public void setServiceImplSerializer(Serializer serviceImplSerializer) {
        this.serviceImplSerializer = serviceImplSerializer;
    }

    public void setActionFactorySerializer(Serializer actionFactorySerializer) {
        this.actionFactorySerializer = actionFactorySerializer;
    }

    public void setModulesSerializer(Serializer modulesSerializer) {
        this.modulesSerializer = modulesSerializer;
    }

    /**
     * @param typeMappings
     */
    public void setTypeMappings(TypeMappings typeMappings) {
        module.bind(TypeMappings.class, typeMappings);
    }

    /**
     * @param columnAnnotations
     */
    public void setColumnAnnotations(boolean columnAnnotations) {
        this.columnAnnotations = columnAnnotations;
    }

    /**
     * @param validationAnnotations
     */
    public void setValidationAnnotations(boolean validationAnnotations) {
        this.validationAnnotations = validationAnnotations;
    }

    /**
     * @param sourceEncoding
     */
    public void setSourceEncoding(String sourceEncoding) {
        this.sourceEncoding = sourceEncoding;
    }

    /**
     * @param schemaToPackage
     */
    public void setSchemaToPackage(boolean schemaToPackage) {
        this.schemaToPackage = schemaToPackage;
        module.bind(SQLCodegenModule.SCHEMA_TO_PACKAGE, schemaToPackage);
    }

    /**
     * @param lowerCase
     */
    public void setLowerCase(boolean lowerCase) {
        this.lowerCase = lowerCase;
    }

    /**
     * @param exportTables
     */
    public void setExportTables(boolean exportTables) {
        this.exportTables = exportTables;
    }

    /**
     * @param exportViews
     */
    public void setExportViews(boolean exportViews) {
        this.exportViews = exportViews;
    }

    /**
     * @param exportAll
     */
    public void setExportAll(boolean exportAll) {
        this.exportAll = exportAll;
    }

    /**
     * @param exportPrimaryKeys
     */
    public void setExportPrimaryKeys(boolean exportPrimaryKeys) {
        this.exportPrimaryKeys = exportPrimaryKeys;
    }

    /**
     * @param exportForeignKeys
     */
    public void setExportForeignKeys(boolean exportForeignKeys) {
        this.exportForeignKeys = exportForeignKeys;
    }

    public void setExportBelongsTos(boolean exportBelongsTos) {
        this.exportBelongsTos = exportBelongsTos;
    }

    public void setExportHasManys(boolean exportHasManys) {
        this.exportHasManys = exportHasManys;
    }

    /**
     * Set the java imports
     *
     * @param imports java imports array
     */
    public void setImports(String[] imports) {
        module.bind(CodegenModule.IMPORTS,
                new HashSet<String>(Arrays.asList(imports)));
    }

    /**
     * @param spatial
     */
    public void setSpatial(boolean spatial) {
        this.spatial = spatial;
    }

    /**
     * @param tableTypesToExport
     */
    public void setTableTypesToExport(String tableTypesToExport) {
        this.tableTypesToExport = tableTypesToExport;
    }

}
